package com.yerbamateprimer.round.state.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.yerbamateprimer.action.system.SystemAction;
import com.yerbamateprimer.action.user.UserAction;
import com.yerbamateprimer.exception.NoneExitFoundException;
import com.yerbamateprimer.exception.UserActionNotExecutedException;
import com.yerbamateprimer.round.Round;
import com.yerbamateprimer.round.state.State;
import com.yerbamateprimer.round.state.StateName;
import com.yerbamateprimer.round.state.StateStatus;
import com.yerbamateprimer.round.state.exit.StateExit;
import com.yerbamateprimer.round.state.exit.StateExitCause;

/**
 * This class models state, it
 * have to be "filled" with:
 * <ul>
 * 		<li>Several optional get in actions.</li>
 * 		<li>An optional delay to auto get out of state.</li>
 * 		<li>Several optional exit of state (If doesn't have an exit it's a dead end state).</li>
 * </ul>
 * @author Matias Liwski
 * @version 1.0
 */
public class CommonState implements State {
	private static final TimeUnit LOCK_TIMEOUT_UNIT = TimeUnit.SECONDS;
	private static final long LOCK_TIMEOUT = 5L;
	private static final Logger LOGGER = LoggerFactory.getLogger(CommonState.class);
	private static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(5);

	private StateName name;
	private StateStatus status;
	private List<SystemAction> getInActions;
	private Long autoGetOutDelay;
	private transient Lock lock;
	private transient ScheduledFuture<?> autoGetOutSchedule = null;
	private Map<StateExitCause, StateExit> exits;
	
	/**
	 * Default constructor, used by the IoC framework.
	 */
	public CommonState() {
		this.exits = new HashMap<StateExitCause, StateExit>();
		this.getInActions = new ArrayList<SystemAction>();
		this.autoGetOutDelay = 0L;
		this.lock = new ReentrantLock();
		this.status = StateStatus.CREATED;
	}

	/**
	 * @param name The name of the state.
	 * @param getInActions The list of system actions to execute when get in the state.
	 * @param autoGetOutDelay The delay before auto get out of state.
	 * @param exits All the possible exits of this state.
	 */
	public CommonState(StateName name, List<SystemAction> getInActions,
			Long autoGetOutDelay, List<StateExit> exits) {
		this();
		setName(name);
		
		for (SystemAction action : getInActions) {
			addGetInAction(action);
		}
		
		setAutoGetOutDelay(autoGetOutDelay);
		
		for (StateExit exit : exits) {
			addExit(exit);
		}
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#execute(com.yerbamateprimer.action.user.UserAction)
	 */
	@Override
	public void execute(UserAction action) {
		Preconditions.checkArgument(action != null, "Can't execute a null action.");
		Preconditions.checkState(StateStatus.INSIDE.equals(this.status) || StateStatus.WAITING.equals(this.status), "Could only execute user actions when are waiting to auto get out (stateStatus: " + this.status + ").");
		
		try {
			if(tryToLock()){
				LOGGER.debug("Executing {} at {}." , action, name);
				action.execute();
			}
		} catch (InterruptedException interruptedException) {
			LOGGER.warn("An exception happend when trying to get lock.", interruptedException);
			throw new RuntimeException(interruptedException);
		} catch (Exception e) {
			LOGGER.warn("An exception happend when trying to execute the action {}.", action, e);
			throw new UserActionNotExecutedException(action.toString(), e);
		} finally {
			tryToUnlock();
		}
	}

	/**
	 * Get into this state. This involves these steps in order:
	 * <ul>
	 * 		<li>Execute the get in system actions.</li>
	 * 		<li>Wait until auto time out is reached or getOut() method is executed.</li>
	 * </ul>
	 * @see com.yerbamateprimer.round.state.State#getIn()
	 */
	@Override
	synchronized public void getIn(Round round) {
		LOGGER.debug("Get into state {}.", getName());
		Preconditions.checkArgument(round != null, "Couldn't get into the state " + name + " with null round.");
		Preconditions.checkState(StateStatus.CREATED.equals(status) || StateStatus.END.equals(status), "Couldn't get into the state " + name + " before complete the cycle.");
		
		try {
			if(tryToLock()){
				this.status = StateStatus.INSIDE;
				round.setCurrentState(this);
				executeGetInActions(round);
				waitBeforeAutoGetOut(round);
			}
		} catch (InterruptedException interruptedException) {
			LOGGER.warn("An exception happend when trying to get lock.", interruptedException);
			throw new RuntimeException(interruptedException);
		} finally {
			tryToUnlock();
		}
	}

	/**
	 * Execute the get in actions.
	 */
	protected void executeGetInActions(Round round) {
		for (SystemAction action : getGetInActions()) {
			LOGGER.debug("Executing " + action + " at " + this.name + " with " + round + ".");
			action.execute(round);
		}
	}

	/**
	 * Wait until some of this hints happen:
	 * <ul>
	 * 		<li>Time out because schedule time is reached.</li>
	 * 		<li>getOut() method is executed.</li>
	 * </ul>
	 */
	protected void waitBeforeAutoGetOut(final Round round){
		this.status = StateStatus.WAITING;
		LOGGER.debug("Sleeping for {} millis, on waiting before get out of state {}.", autoGetOutDelay, name);
		
		Runnable executeGetOut = new TimeOutTask(round);
		this.autoGetOutSchedule = EXECUTOR.schedule( executeGetOut, autoGetOutDelay, TimeUnit.MILLISECONDS);
	}

	/**
	 * Force the get out of this state. This involves these steps in order:
	 * <ul>
	 * 		<li>Recognize the exit cause.</li>
	 * 		<li>Execute the get out system actions, corresponding to the exit cause with each delay.</li>
	 * 		<li>Get into the next state if there's one.</li>
	 * </ul>
	 * 
	 * @see com.yerbamateprimer.round.state.State#getOut() 
	 */
	@Override
	synchronized public void getOut(Round round) {
		Preconditions.checkState(StateStatus.WAITING.equals(this.status) || StateStatus.TIMED_OUT.equals(status), "Couldn't get out of state " + name + " if doesn't get into it (stateStatus: " + status + ").");
		
		try {
			if(tryToLock()) { 
				if(!StateStatus.TIMED_OUT.equals(status)) {
					this.autoGetOutSchedule.cancel(true);
					this.status = StateStatus.INTERRUPTED;
				}
				
				LOGGER.debug("Get out of state {}, with status of {}.", name, status);
				
				for (StateExit exit : this.exits.values() ) {
					if (exit.isThisExit(this)) {
						exit.apply(round);
						return;
					}
				}
				
				if(!this.exits.isEmpty()){
					throw new NoneExitFoundException();
				}
			}
		} catch (InterruptedException interruptedException) {
			LOGGER.warn("An exception happend when trying to get lock.", interruptedException);
			throw new RuntimeException(interruptedException);
		} finally {
			this.status = StateStatus.END;
			tryToUnlock();
		}
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#setName(com.yerbamateprimer.round.state.StateName)
	 */
	@Override
	public void setName(StateName stateName) {
		Preconditions.checkArgument(stateName != null, "Could set a null name.");
		this.name = stateName;
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#getName()
	 */
	@Override
	public StateName getName() {
		return this.name;
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#getStatus()
	 */
	@Override
	public StateStatus getStatus() {
		return this.status;
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#addGetInAction(com.yerbamateprimer.action.system.SystemAction)
	 */
	@Override
	public void addGetInAction(SystemAction action) {
		Preconditions.checkArgument(action != null, "Couldn't add a null system action to {} state.", this.name);
		this.getInActions.add(action);
	}
	
	/**
	 * @return A safe copy, of the list of system actions to execute when get in the state.
	 * @see com.yerbamateprimer.round.state.State#getGetInActions()
	 */
	@Override
	public List<SystemAction> getGetInActions() {
		return new ArrayList<SystemAction>(this.getInActions);
	}
	
	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#setAutoGetOutDelay(java.lang.Long)
	 */
	@Override
	public void setAutoGetOutDelay(Long delay) {
		Preconditions.checkArgument(delay != null && delay >= 0, "The auto get out delay, has to be gratter than zero (delay: " + delay + ").");
		this.autoGetOutDelay = delay;
	}
	
	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#getAutoGetOutDelay()
	 */
	@Override
	public Long getAutoGetOutDelay() {
		return this.autoGetOutDelay;
	}

	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#addExit(com.yerbamateprimer.round.state.exit.StateExit)
	 */
	@Override
	public void addExit(StateExit exit) {
		Preconditions.checkArgument(exit != null, "Could not add a null exit.");
		Preconditions.checkArgument(exit.getCause() != null, "Could not add an exit that havn't an exit cause.");
		this.exits.put(exit.getCause(), exit);
	}
	
	/* (non-Javadoc)
	 * @see com.yerbamateprimer.round.state.State#getExit(com.yerbamateprimer.round.state.exit.StateExitCause)
	 */
	@Override
	public StateExit getExit(StateExitCause exitCause) {
		return exits.get(exitCause);
	}

	/**
	 * @return A safe copy, of all the possible exits of this state.
	 * @see com.yerbamateprimer.round.state.State#getExits()
	 */
	@Override
	public List<StateExit> getExits() {
		return new ArrayList<StateExit>(this.exits.values());
	}

	/**
	 * @return The ScheduledFuture task for auto get out.
	 */
	protected ScheduledFuture<?> getAutoGetOutSchedule() {
		return this.autoGetOutSchedule;
	}

	/**
	 * @return The state lock for actions.
	 */
	protected Lock getLock() {
		return this.lock;
	}

	/**
	 * Try to lock the lock but if after LOCK_TIMEOUT this couldn't be done throws an IllegalMonitorStateException. 
	 */
	private boolean tryToLock() throws InterruptedException {
		LOGGER.debug("Triying to lock: " + lock + " for " + LOCK_TIMEOUT + " " + LOCK_TIMEOUT_UNIT + ".");
		return lock.tryLock(LOCK_TIMEOUT, LOCK_TIMEOUT_UNIT);
	}

	/**
	 * If the lock is locked unlock it.
	 */
	private void tryToUnlock() {
		LOGGER.debug("Triying to unlock: {}.", lock);
		if(((ReentrantLock)lock).isLocked() && ((ReentrantLock)lock).isHeldByCurrentThread() ){
			lock.unlock();
			LOGGER.debug("Unlocked: {}.", lock);
		}
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE, false, true);
	}

	/**
	 * Helper class to run the time out task.
	 */
	private class TimeOutTask implements Runnable {
		private final Round round;
		
		public TimeOutTask(Round round) {
			super();
			this.round = round;
		}

		public void run() {
			LOGGER.debug("Waked up after {} millis, on wait before auto get out of {} state.", autoGetOutDelay, name);
			try {
				if(tryToLock()) { 
					status = StateStatus.TIMED_OUT;
					getOut(round);
				}
			} catch (InterruptedException interruptedException) {
				LOGGER.warn("An exception happend when trying to get lock.", interruptedException);
				throw new RuntimeException(interruptedException);
			} finally {
				tryToUnlock();
			}

		}

		/* (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE, false, true);
		}

	}
}